package eu.catengine.scene;

import java.util.ArrayList;

import eu.catengine.CatEngineCore;
import eu.catengine.common.iface.IDisposable;
import eu.catengine.common.listener.CameraListener;
import eu.catengine.common.manager.ScreenManager;
import eu.catengine.primitive.Point;
import eu.catengine.primitive.Vector;

/**
 * Klasa przechowująca dane dotyczące pozycji kamery w scenie. <br>
 * <br>
 * Punt zaczepienia obiektu (anchor point) znajduje się w lewym dolnym rogu.
 * 
 * @author mbienkowski
 */
public class Camera implements IDisposable
{
    public static enum CameraActionType
    {
	moveBy, moveTo
    };

    private Point mPoint;
    private float mPositionLimit[] = new float[] { -99999999f, 99999999f, -99999999f, 99999999f };

    private Scene mScene;
    private Camera mInstance;
    private long mElapsedTime;
    private ArrayList<CameraEffect> mInterpActionList;
    private CameraListener mCameraListener;

    /**
     * Tworzy kamerę.
     * 
     * @param scene
     *            <code>Scene</code> Scena w której znajduje się kamera.
     */
    public Camera(Scene scene)
    {
	mScene = scene;
	mPoint = new Point();
	mInterpActionList = new ArrayList<Camera.CameraEffect>();

	mInstance = this;
    }

    /**
     * Tworzy kamerę.
     * 
     * @param scene
     *            <code>Scene</code> Scena w której znajduje się kamera.
     * @param point
     *            <code>Point</code> Punkt zaczepienia.
     */
    public Camera(Scene scene, Point point)
    {
	mScene = scene;
	mPoint.x = (Math.max(mPositionLimit[0], Math.min(point.x, mPositionLimit[1])));
	mPoint.y = (Math.max(mPositionLimit[2], Math.min(point.y, mPositionLimit[3])));
	mInterpActionList = new ArrayList<Camera.CameraEffect>();

	mInstance = this;
    }

    /**
     * Tworzy kamerę.
     * 
     * @param scene
     *            <code>Scene</code> Scena w której znajduje się kamera.
     * @param x
     *            <code>float</code> Współrzędna X punktu zaczepienia.
     * @param y
     *            <code>float</code> Współrzędna Y punktu zaczepienia.
     */
    public Camera(Scene scene, float x, float y)
    {
	mScene = scene;
	mPoint.x = (Math.max(mPositionLimit[0], Math.min(x, mPositionLimit[1])));
	mPoint.y = (Math.max(mPositionLimit[2], Math.min(y, mPositionLimit[3])));
	mInterpActionList = new ArrayList<Camera.CameraEffect>();

	mInstance = this;
    }

    /**
     * Tworzy kamerę.
     * 
     * @param scene
     *            <code>Scene</code> Scena w której znajduje się kamera.
     * @param point
     *            <code>Point</code> Punkt zaczepienia.
     * @param minX
     *            <code>float</code> Minimalna wartość współrzędnej osi X w
     *            której może znaleźć się kamera.
     * @param maxX
     *            <code>float</code> Maksymalna wartość współrzędnej osi X w
     *            której może znaleźć się kamera.
     * @param minY
     *            <code>float</code> Minimalna wartość współrzędnej osi Y w
     *            której może znaleźć się kamera.
     * @param maxY
     *            <code>float</code> Maksymalna wartość współrzędnej osi Y w
     *            której może znaleźć się kamera.
     */
    public Camera(Scene scene, Point point, float minX, float maxX, float minY, float maxY)
    {
	mPositionLimit[0] = minX;
	mPositionLimit[1] = maxX;
	mPositionLimit[2] = minY;
	mPositionLimit[3] = maxY;

	mScene = scene;
	mPoint.x = (Math.max(mPositionLimit[0], Math.min(point.x, mPositionLimit[1])));
	mPoint.y = (Math.max(mPositionLimit[2], Math.min(point.y, mPositionLimit[3])));
    }

    /**
     * Tworzy kamerę.
     * 
     * @param scene
     *            <code>Scene</code> Scena w której znajduje się kamera.
     * @param x
     *            <code>float</code> Współrzędna X punktu zaczepienia.
     * @param y
     *            <code>float</code> Współrzędna Y punktu zaczepienia.
     * @param minX
     *            <code>float</code> Minimalna wartość współrzędnej osi X w
     *            której może znaleźć się kamera.
     * @param maxX
     *            <code>float</code> Maksymalna wartość współrzędnej osi X w
     *            której może znaleźć się kamera.
     * @param minY
     *            <code>float</code> Minimalna wartość współrzędnej osi Y w
     *            której może znaleźć się kamera.
     * @param maxY
     *            <code>float</code> Maksymalna wartość współrzędnej osi Y w
     *            której może znaleźć się kamera.
     */
    public Camera(Scene scene, float x, float y, float minX, float maxX, float minY, float maxY)
    {
	mPositionLimit[0] = minX;
	mPositionLimit[1] = maxX;
	mPositionLimit[2] = minY;
	mPositionLimit[3] = maxY;

	mScene = scene;
	mPoint.x = (Math.max(mPositionLimit[0], Math.min(x, mPositionLimit[1])));
	mPoint.y = (Math.max(mPositionLimit[2], Math.min(y, mPositionLimit[3])));
    }

    /**
     * Zmienia pozycję kamery do danego punktu.
     * 
     * @param point
     *            <code>Point</code> Punkt docelowy.
     */
    public void moveTo(Point point)
    {
	point.x = (Math.max(mPositionLimit[0], Math.min(point.x, mPositionLimit[1])));
	point.y = (Math.max(mPositionLimit[2], Math.min(point.y, mPositionLimit[3])));

	Point diff = new Point(point.x - mPoint.x, point.y - mPoint.y);

	mPoint = point;

	if (mCameraListener != null)
	{
	    mCameraListener.onPositionChanged(mInstance, diff.x, diff.y);
	}
    }

    /**
     * Zmienia pozycję kamery do danego punktu w zadanym czasie.
     * 
     * @param point
     *            <code>Point</code> Punkt docelowy.
     * @param time
     *            <code>long</code> Czas podany w ms.
     */
    public void moveTo(Point point, long time)
    {
	mInterpActionList.add(new SingleAction(point.x, point.y, time, CameraActionType.moveTo));
    }

    /**
     * Zmienia pozycję kamery do danego punktu.
     * 
     * @param x
     *            <code>float</code> Współrzędna X punktu docelowego.
     * @param y
     *            <code>float</code> Współrzędna Y punktu docelowego.
     */
    public void moveTo(float x, float y)
    {
	x = (Math.max(mPositionLimit[0], Math.min(x, mPositionLimit[1])));
	y = (Math.max(mPositionLimit[2], Math.min(y, mPositionLimit[3])));

	Point diff = new Point(x - mPoint.x, y - mPoint.y);

	mPoint.x = x;
	mPoint.y = y;

	if (mCameraListener != null)
	{
	    mCameraListener.onPositionChanged(mInstance, diff.x, diff.y);
	}
    }

    /**
     * Zmienia pozycję kamery do danego punktu w zadanym czasie.
     * 
     * @param x
     *            <code>float</code> Współrzędna X punktu docelowego.
     * @param y
     *            <code>float</code> Współrzędna Y punktu docelowego.
     * @param time
     *            <code>long</code> Czas podany w ms.
     */
    public void moveTo(float x, float y, long time)
    {
	mInterpActionList.add(new SingleAction(x, y, time, CameraActionType.moveTo));
    }

    /**
     * Zmienia pozycję kamery o dany punkt.
     * 
     * @param point
     *            <code>Point</code> Punkt docelowy.
     */
    public void moveBy(Point point)
    {
	point.x = (Math.max(mPositionLimit[0], Math.min(this.mPoint.x - point.x, mPositionLimit[1])));
	point.y = (Math.max(mPositionLimit[2], Math.min(this.mPoint.y + point.y, mPositionLimit[3])));

	Point diff = new Point(point.x - mPoint.x, point.y - mPoint.y);

	mPoint = point;

	if (mCameraListener != null)
	{
	    mCameraListener.onPositionChanged(mInstance, diff.x, diff.y);
	}
    }

    /**
     * Zmienia pozycję kamery o dany punkt w zadanym czasie.
     * 
     * @param point
     *            <code>Point</code> Punkt docelowy.
     * @param time
     *            <code>long</code> Czas podany w ms.
     */
    public void moveBy(Point point, long time)
    {
	mInterpActionList.add(new SingleAction(point.x, point.y, time, CameraActionType.moveBy));
    }

    /**
     * Zmienia pozycję kamery o dany punkt.
     * 
     * @param x
     *            <code>float</code> Współrzędna X punktu docelowego.
     * @param y
     *            <code>float</code> Współrzędna Y punktu docelowego.
     */
    public void moveBy(float x, float y)
    {
	x = (Math.max(mPositionLimit[0], Math.min(this.mPoint.x - x, mPositionLimit[1])));
	y = (Math.max(mPositionLimit[2], Math.min(this.mPoint.y + y, mPositionLimit[3])));

	Point diff = new Point(x - mPoint.x, y - mPoint.y);

	mPoint.x = x;
	mPoint.y = y;

	if (mCameraListener != null)
	{
	    mCameraListener.onPositionChanged(mInstance, diff.x, diff.y);
	}
    }

    /**
     * Zmienia pozycję kamery o dany punkt w zadanym czasie.
     * 
     * @param x
     *            <code>float</code> Współrzędna X punktu docelowego.
     * @param y
     *            <code>float</code> Współrzędna Y punktu docelowego.
     * @param time
     *            <code>long</code> Czas podany w ms.
     */
    public void moveBy(float x, float y, long time)
    {
	mInterpActionList.add(new SingleAction(x, y, time, CameraActionType.moveBy));
    }

    /**
     * * @param minX <code>float</code> Minimalna wartość współrzędnej osi X w
     * której może znaleźć się kamera.
     * 
     * @param maxX
     *            <code>float</code> Maksymalna wartość współrzędnej osi X w
     *            której może znaleźć się kamera.
     * @param minY
     *            <code>float</code> Minimalna wartość współrzędnej osi Y w
     *            której może znaleźć się kamera.
     * @param maxY
     *            <code>float</code> Maksymalna wartość współrzędnej osi Y w
     *            której może znaleźć się kamera.
     */
    public void setPositionLimit(float minX, float maxX, float minY, float maxY)
    {
	mPositionLimit[0] = minX;
	mPositionLimit[1] = maxX;
	mPositionLimit[2] = minY;
	mPositionLimit[3] = maxY;

	mPoint.x = (Math.max(mPositionLimit[0], Math.min(mPoint.x, mPositionLimit[1])));
	mPoint.y = (Math.max(mPositionLimit[2], Math.min(mPoint.y, mPositionLimit[3])));
    }

    /**
     * Dodaje obiekt nasłuchujący do kamery.
     * 
     * @param listener
     *            <code>CameraListener</code> obiekt nasłuchujący.
     */
    public void setCameraListener(CameraListener listener)
    {
	mCameraListener = listener;
    }

    /**
     * Zwraca aktualny punkt zaczepienia kamery.
     * 
     * @return <code>Point</code> pozycja kamery.
     */
    public Point getAnchorPoint()
    {
	return new Point(mPoint);
    }

    /**
     * Zwraca punkt w którym znajduję się środek kamery.
     * 
     * @return <code>Point</code>pozycja środka kamery.
     */
    public Point getCenterPoint()
    {
	return new Point(mPoint).add(ScreenManager.getInstance().getScreenWidth() / 2, -ScreenManager.getInstance().getScreenHeight() / 2);
    }

    /**
     * Zwraca współrzędną osi X dla punktu zaczepienia.
     * 
     * @return <code>float</code> Wspołrzędna X.
     */
    public float getX()
    {
	return mPoint.x;
    }

    /**
     * Zwraca współrzędną osi Y dla punktu zaczepienia.
     * 
     * @return <code>float</code> Wspołrzędna Y.
     */
    public float getY()
    {
	return mPoint.y;
    }

    /**
     * Aktualizacja elementów wywoływana z pętli gównej w scenie. <br>
     * <b>Metoda wywoływana jest automatycznie.</b><br>
     * Nie należy wywoływać metody ręcznie.
     */
    public void update()
    {
	mElapsedTime = mScene.getElapsedTime();

	for (int i = mInterpActionList.size() - 1; i >= 0; i--)
	{
	    mInterpActionList.get(i).update();
	}
    }

    /**
     * Zatrzymuje wszystkie akcje kamery.
     */
    public void stopCameraAction()
    {
	CameraEffect ce;

	for (int i = mInterpActionList.size() - 1; i >= 0; i--)
	{
	    ce = mInterpActionList.get(i);

	    mInterpActionList.remove(i);

	    if (mCameraListener != null)
	    {
		mCameraListener.onActionStopped(mInstance, ce.getType());
	    }
	}
    }

    /**
     * Zatrzymuje akcje kamery.
     * 
     * @param type
     *            <code>CameraActionType</code> Typ akcji do zatrzymania.
     */
    public void stopCameraAction(CameraActionType type)
    {
	CameraEffect ce;

	for (int i = mInterpActionList.size() - 1; i >= 0; i--)
	{
	    ce = mInterpActionList.get(i);

	    if (ce.getType().equals(type))
	    {
		mInterpActionList.remove(i);

		if (mCameraListener != null)
		{
		    mCameraListener.onActionStopped(mInstance, type);
		}
	    }
	}
    }

    private abstract class CameraEffect
    {
	protected CameraActionType mType;
	protected long mStartTime;
	protected int mActualFrame, mKeyFrames;

	public CameraEffect(long duration)
	{
	    if (duration == 0)
	    {
		duration = CatEngineCore.getFrameDuration();
	    }

	    mActualFrame = 0;
	    mStartTime = mScene.getElapsedTime();
	    mKeyFrames = (int) Math.ceil(duration / CatEngineCore.getFrameDuration());
	}

	public CameraActionType getType()
	{
	    return mType;
	}

	public void update()
	{
	    long time = mElapsedTime - mStartTime;

	    if (time > 0 && mActualFrame < mKeyFrames)
	    {
		mActualFrame++;
		updateForward();
	    }
	    else if (mActualFrame == mKeyFrames)
	    {
		mInterpActionList.remove(this);

		if (mCameraListener != null)
		{
		    mCameraListener.onActionEnded(mInstance, mType);
		}
	    }
	    else if (time < 0 && mActualFrame > 0)
	    {
		mActualFrame--;
		updateBackward();
	    }
	}

	abstract public void updateBackward();

	abstract public void updateForward();
    }

    class SingleAction extends CameraEffect
    {
	Vector mDeltaVector;

	public SingleAction(float x, float y, long duration, CameraActionType type)
	{
	    super(duration);
	    mType = type;

	    if (mType == CameraActionType.moveBy)
	    {
		mDeltaVector = new Vector(mPoint, new Point(mPoint.x + x, mPoint.y + y));
	    }
	    else if (mType == CameraActionType.moveTo)
	    {
		mDeltaVector = new Vector(mPoint, new Point(x, y));
	    }

	    mDeltaVector.divide((float) mKeyFrames);
	}

	public void updateForward()
	{
	    if (mType == CameraActionType.moveBy)
	    {
		mInstance.moveBy(mDeltaVector.x, mDeltaVector.y);
	    }
	    else if (mType == CameraActionType.moveTo)
	    {
		mInstance.moveBy(mDeltaVector.x, mDeltaVector.y);
	    }
	}

	public void updateBackward()
	{
	    if (mType == CameraActionType.moveBy)
	    {
		mInstance.moveBy(-mDeltaVector.x, -mDeltaVector.y);
	    }
	    else if (mType == CameraActionType.moveTo)
	    {
		mInstance.moveBy(-mDeltaVector.x, -mDeltaVector.y);
	    }
	}
    }

    @Override
    public void dispose()
    {
	mInterpActionList.clear();
    }
}
